﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Runtime.InteropServices;
using System.Reflection;
using Un4seen.Bass;
using Un4seen.Bass.AddOn.Tags;
using Un4seen.Bass.AddOn.Mix;
using sdd.utilities;

// SDD# - Software Defined Devices in C# for Linux and Windows
// AudioStreamer handles InetStreams
// (c) 2014-2015 Chris Burri. MIT License applies. 
// See http://niston.wordpress.com

namespace sdd.subsystems.audio
{
    public class AudioStreamer
    {

    #region Member Vars
        // HTTP User Agent related stuff 
        private static string _userAgent = "NISTON AudioStreamer/1.0 (" + Assembly.GetExecutingAssembly().GetName().Name + " " + Assembly.GetExecutingAssembly().GetName().Version.ToString() + "; " + Environment.OSVersion + ")";
        private IntPtr _userAgentPtr;
        // Our collection of InetStreams
        private List<InetStream> _Streams;
        // Handle to our output mixer stream
        private int _MixerStream;
        // VOODOO (see Start() method)
        private bool performVoodoo = true;

    #endregion

    #region Constructor/Destructor

        public AudioStreamer(int numberOfChannels, int maxNumberOfStreams)
        {
            // setup HTTP User Agent pointer
            _userAgentPtr = Marshal.StringToHGlobalAnsi(_userAgent);

            // BASS setup / init
            SetupBassLibrary();
            InitBassLibrary(FindDeviceId());

            // our streams
            _Streams = new List<InetStream>();
            MaximumStreams = maxNumberOfStreams;            

            // BLACK MAGIC: persuade mono not to crash :)
            var x = Bass.BASS_GetVersion().ToString();

            // broken
            //int sampleRate = FindCurrentDeviceSamplerate();
            
            // this is our output mixer
            _MixerStream = BassMix.BASS_Mixer_StreamCreate(44100, numberOfChannels, BASSFlag.BASS_MIXER_NONSTOP | (FloatingPointSupport ? BASSFlag.BASS_SAMPLE_FLOAT : 0));

            // play back the mixer channel to the default audio device
            Bass.BASS_ChannelPlay(_MixerStream, true);
            Bass.BASS_ChannelSetAttribute(_MixerStream, BASSAttribute.BASS_ATTRIB_VOL, 1);

        }

        ~AudioStreamer()
        {
            // stop streaming
            StopAll(true);   

            // destroy user agent pointer
            Marshal.FreeHGlobal(_userAgentPtr);
         
            // stop BASS audio
            Bass.BASS_Stop();
            Bass.BASS_Free();   
        }

    #endregion

    #region Public Interface

        // events
        public event EventHandler<StreamChangingEventArgs> StreamChanging;
        public event EventHandler<StreamChangedEventArgs> StreamChanged;
        public event EventHandler StreamEnded;
        public event EventHandler MetadataUpdated;

        // Bass Net Licensing Information
        public string BassNetRegEMail { set; get; }
        public string BassNetRegKey { set; get; }

        // 
        public int MaximumStreams { set; get; }
        public bool FloatingPointSupport { private set; get; }
        public InetStream CurrentStream { private set; get; }
        public int DeviceNumber { set; get; }
        public string UserAgentApplication { set; get; }

        
        // start a new stream from streamURL
        public InetStream Start(string streamURL, bool dontThrowBassException = false, bool dontPlayback = false)
        {
            Console.Write("Start(): " + streamURL + ", dontThrowBassException=" + dontThrowBassException.ToString() + ", dontPlayback=" + dontPlayback.ToString());
            
            // New InetStream for the new URL to start
            InetStream newStream = new InetStream();

            // register event handlers
            newStream.StreamEnd += newStream_OnStreamEnd;
            newStream.StreamStart += newStream_OnStreamStart;
            newStream.StreamPlaying += newStream_OnStreamPlaying;
            newStream.StreamHTTP += newStream_OnStreamHTTP;
            newStream.MetadataUpdated += newStream_OnMetadataUpdated;

            // add backreference 
            newStream.Parent = this;

            // raise stream changing event
            if (StreamChanging != null) { StreamChanging(this, new StreamChangingEventArgs(CurrentStream, newStream)); }

            try
            {
                // open the new stream
                newStream.Open(streamURL);

                // add the stream to our stream collection
                _Streams.Add(newStream);

                // set new stream to muted state (will fade in on stream start event)
                // VOODOO: First fade-in fails for some unknown reason, so don't set volume to zero the first time a stream is started
                if (!performVoodoo) { Bass.BASS_ChannelSetAttribute(newStream.Handle, BASSAttribute.BASS_ATTRIB_VOL, 0); }
                performVoodoo = false;

                System.Diagnostics.Debug.Print("Streamer: Stream Created, I have ({0}) streams now.", _Streams.Count);

                // all is good
                return newStream;
            }
            catch (BASSException e)
            {
                // connection iz FAIL :(
                if (!dontThrowBassException ) { throw e; }
                return null;
            }
        }

        void newStream_OnMetadataUpdated(object sender, EventArgs e)
        {
            if (MetadataUpdated != null) { MetadataUpdated(this, new EventArgs()); }
        }

        // stop everything
        public void StopAll(bool stopImmediately = false, bool excludeCurrentStream = false)
        {           
            // stop and remove all streams  (exception excludeCurrentStream)
            while ((!excludeCurrentStream & _Streams.Count > 0) | (excludeCurrentStream & _Streams.Count > 1))
            {
                InetStream first = _Streams.First();
                if (!excludeCurrentStream | first.Handle == this.CurrentStream.Handle)
                {
                    first.Stop(stopImmediately);
                    if (first.Handle == CurrentStream.Handle) { UnregisterEventHandlers(CurrentStream); }
                    first.Parent = null;
                    _Streams.Remove(first);
                }
            }
            
            // clear reference to current stream
            CurrentStream = null;

            // signal stream end
            if (StreamEnded != null) { StreamEnded(this, new EventArgs()); }
          

        }


        public AudioLevels AudioLevels
        {
            get
            {
                // get audio levels (L/R)
                return new AudioLevels(Bass.BASS_ChannelGetLevel(_MixerStream));
            }
        }
        
        public List<string> Devices
        {
            get 
            {
                List<string> devices = new List<string>();
                BASS_DEVICEINFO info;
                for (int n = 1; (info = Bass.BASS_GetDeviceInfo(n)) != null; n++)
                {
                    devices.Add(info.name);
                }
                return devices;
            }
        }

        // get a name for the stream type
        public string StreamFormatString
        {
            get
            {
                if (CurrentStream != null)
                {
                    switch (CurrentStream.ChannelType)
                    {
                        case BASSChannelType.BASS_CTYPE_STREAM_AAC:
                            return "AAC";
                        case BASSChannelType.BASS_CTYPE_STREAM_MP3:
                            return "MP3";
                        case BASSChannelType.BASS_CTYPE_STREAM_OGG:
                            return "OGG";
                        case BASSChannelType.BASS_CTYPE_STREAM_OPUS:
                            return "OPUS";
                        case BASSChannelType.BASS_CTYPE_STREAM_MF:
                            return ""; // can't determine - windows media foundation codec
                        default:
                            //return CurrentStream.ChannelType.ToString();
                            return "";
                    }
                }
                else
                {
                    return "";
                }

            }
        }

        // get the samplerate string
        public string SamplerateString
        {
            get
            {
                if (CurrentStream != null)
                {
                    switch (CurrentStream.SampleRate)
                    {
                        case 8000:
                            return "8k";
                        case 16000:
                            return "16k";
                        case 22050:
                            return "22k5";
                        case 44100:
                            return "44k1";
                        case 48000:
                            return "48k";
                        case 96000:
                            return "96k";
                        case 192000:
                            return "192k";
                        case 384000:
                            return "384k";
                        default:
                            return CurrentStream.SampleRate / 1000 + "kHz";
                    }
                }
                else
                {
                    return "";
                }
            }
        }

        // format Song Line
        public string FormattedSongLine
        {
            get
            {
                if (CurrentStream != null)
                {
                    var a = !(string.IsNullOrWhiteSpace(CurrentStream.Artist));
                    var t = !(string.IsNullOrWhiteSpace(CurrentStream.Title));

                    return (a ? (t ? CurrentStream.Artist + " - " + CurrentStream.Title : CurrentStream.Artist) : (t ? CurrentStream.Title : ""));
                }
                else
                {
                    return "";
                }
            }
        }

    #endregion

    #region Private Functions

        // BASS Setup
        private void SetupBassLibrary()
        {
            // we must register BASS.NET to be able to run in console, otherwise will crash on Unix without X Server
            // get a freeware key by registering on http://bass.radio42.com/ and put the info into bassnet.lic
            // bassnet.lic file must contain a single line with email address and registry key separated by a space
            BassNetLic.BassNetLicInfo licInfo = BassNetLic.GetBassNetLicInfo();
            if (licInfo != null) { BassNet.Registration(licInfo.EMail, licInfo.RegKey); }

            // System specific path separator
            var dirSep = System.IO.Path.DirectorySeparatorChar;
            
            // load BASS plugins from codecs subdirectory
            Bass.BASS_PluginLoadDirectory(Environment.CurrentDirectory + dirSep + "codecs" + dirSep);
            
            // set BASS configuration
            Bass.BASS_SetConfig(BASSConfig.BASS_CONFIG_NET_TIMEOUT, 7000);                  // connection timeout 7000ms
            Bass.BASS_SetConfig(BASSConfig.BASS_CONFIG_NET_READTIMEOUT, 15000);             // network read timeout 15000ms
            Bass.BASS_SetConfig(BASSConfig.BASS_CONFIG_NET_BUFFER, 10000);                  // network buffer size 10000ms
            Bass.BASS_SetConfig(BASSConfig.BASS_CONFIG_NET_PREBUF, 30);                     // prebuffer 30% of buffer size
            Bass.BASS_SetConfig(BASSConfig.BASS_CONFIG_NET_PLAYLIST, true);                 // enable playlist processing
            Bass.BASS_SetConfigPtr(BASSConfig.BASS_CONFIG_NET_AGENT, _userAgentPtr);        // set custom HTTP User Agent
            
        }

        // find playback device id to use
        private int FindDeviceId()
        {
            BASS_DEVICEINFO info;
            for (int n=1; (info = Bass.BASS_GetDeviceInfo(n)) != null; n++)
            {
                // this is crude. better idea, anyone???
                if (info.name.ToUpper().Contains("USB"))
                {
                    // USB devices preferred (speakers, headphones)
                    return n;
                }
            }
            // no USB device found, use default (Pi-DAC or PC Soundcard)
            return -1;
        }

        private int FindCurrentDeviceSamplerate()
        {
            BASS_INFO info = Bass.BASS_GetInfo();
            return info.maxrate;
        }

        // init Bass Library on given device (defaults to default device)
        private void InitBassLibrary(int deviceID = -1)
        {
            // might have to change hardcoded 44k1 to something more configurable
            if (Bass.BASS_Init(deviceID, 44100, BASSInit.BASS_DEVICE_DEFAULT, IntPtr.Zero))
            {
                // init succeeded, check floating point support
                this.FloatingPointSupport = CheckFloatSupport();
            }
            else
            {
                // init failed
                this.FloatingPointSupport = false;
                throw new BASSException();
            }
        }

        private bool CheckFloatSupport()
        {
            // check FLOAT support                
            int streamChk = Bass.BASS_StreamCreate(44100, 1, BASSFlag.BASS_SAMPLE_FLOAT, null, IntPtr.Zero);
            if (streamChk == 0) { return false; } else { Bass.BASS_StreamFree(streamChk); return true; }
        }

        //// get stream type name from BASSChannelType
        //static string GetStreamFormatString(Un4seen.Bass.BASSChannelType ctype)
        //{
        //    switch (ctype)
        //    {
        //        case BASSChannelType.BASS_CTYPE_STREAM_AAC:
        //            return "AAC";
        //        case BASSChannelType.BASS_CTYPE_STREAM_MP3:
        //            return "MP3";
        //        case BASSChannelType.BASS_CTYPE_STREAM_OGG:
        //            return "OGG";
        //        case BASSChannelType.BASS_CTYPE_STREAM_OPUS:
        //            return "OPUS";
        //        default:
        //            return ctype.ToString();
        //    }
        //}


        void newStream_OnStreamHTTP(object sender, EventArgs e)
        {
            // http information received
            var s = (InetStream)sender;
            Console.WriteLine("OnStreamHttp: " + s.URL);
        }

        void newStream_OnStreamPlaying(object sender, EventArgs e)
        {
            // new stream connected OK and has begun playing
            InetStream newStream = (InetStream)sender;
            InetStream oldStream = this.CurrentStream;

            Console.WriteLine("OnStreamPlaying: " + newStream.URL);

            // slide new stream volume from 0 to 100%
            if (Bass.BASS_ChannelSlideAttribute(newStream.Handle, BASSAttribute.BASS_ATTRIB_VOL, 1, 600))
            { Console.WriteLine("Fading in new stream..."); }
            else
            { Console.WriteLine("Fade-in failed!"); }

            // slide current stream volume from 100 to 0% and stop/autofree (-1 parameter)
            if (this.CurrentStream != null) { Bass.BASS_ChannelSlideAttribute(this.CurrentStream.Handle, BASSAttribute.BASS_ATTRIB_VOL, -1, 400); Console.WriteLine("Fading out Current Stream..."); }

            // new stream is now current stream
            this.CurrentStream = newStream;

            // raise StreamChanged event if we have a new stream
            if (StreamChanged != null && newStream != null) { StreamChanged(this, new StreamChangedEventArgs(oldStream, newStream)); Console.WriteLine("Stream Changed."); }


            //BASS_MIXER_NODE[] FadeInCurve = 
            //{
            //    new BASS_MIXER_NODE(Bass.BASS_ChannelSeconds2Bytes(_mixerStream, 0d), 0f),
            //    new BASS_MIXER_NODE(Bass.BASS_ChannelSeconds2Bytes(_mixerStream, 0.1d), 0.1f),
            //    new BASS_MIXER_NODE(Bass.BASS_ChannelSeconds2Bytes(_mixerStream, 0.15d), 0.25f),
            //    new BASS_MIXER_NODE(Bass.BASS_ChannelSeconds2Bytes(_mixerStream, 0.25d), 0.5f),
            //    new BASS_MIXER_NODE(Bass.BASS_ChannelSeconds2Bytes(_mixerStream, 0.45d), 0.85f),
            //    new BASS_MIXER_NODE(Bass.BASS_ChannelSeconds2Bytes(_mixerStream, 5d), 1f)
            //};

            //var res = BassMix.BASS_Mixer_ChannelSetEnvelope(_currentStreamHandle, BASSMIXEnvelope.BASS_MIXER_ENV_VOL, FadeInCurve);            
        }

        void newStream_OnStreamStart(object sender, EventArgs e)
        {
            // connecting
            var s = (InetStream)sender;
            Console.WriteLine("OnStreamStart: " + s.URL);
            System.Diagnostics.Debug.Print("Streamer: New stream created from ({0})...", s.URL);
            BassMix.BASS_Mixer_StreamAddChannel(_MixerStream, s.Handle, BASSFlag.BASS_MIXER_DOWNMIX | BASSFlag.BASS_STREAM_AUTOFREE | BASSFlag.BASS_MIXER_BUFFER);
            Console.WriteLine("New stream added to mixer.");

        }

        void newStream_OnStreamEnd(object sender, EventArgs e)
        {
            var stream = (InetStream)(sender);
            Console.WriteLine("OnStreamEnd: " + stream.URL);

            if (stream.Handle == CurrentStream.Handle)
            {
                // current stream has ended!
                if (StreamEnded != null) { StreamEnded(this, new EventArgs()); }
            }
            else
            {
                // some other stream has ended, we don't care
            }

            // remove event handlers for the stream
            UnregisterEventHandlers(stream);
            
            // unreference streamer
            stream.Parent = null;

            // remove stream from list
            _Streams.Remove(stream);

            System.Diagnostics.Debug.Print("Stream Ended: I have ({0}) streams now.", _Streams.Count);
        }

        private void UnregisterEventHandlers(InetStream stream)
        {
            // unregister event handlers
            if (stream != null)
            {
                stream.StreamStart -= newStream_OnStreamStart;
                stream.StreamPlaying -= newStream_OnStreamPlaying;
                stream.StreamHTTP -= newStream_OnStreamHTTP;
                stream.MetadataUpdated -= newStream_OnMetadataUpdated;
                stream.StreamEnd -= newStream_OnStreamEnd;
            }
        }
    #endregion

        #region Nested Types

        public class StreamChangedEventArgs : EventArgs
        {
            public InetStream PreviousStream { private set; get; }
            public InetStream CurrentStream { private set; get; }
            public StreamChangedEventArgs(InetStream previousStream, InetStream currentStream)
            {
                this.PreviousStream = previousStream;
                this.CurrentStream = currentStream;
            }
        }

        public class StreamChangingEventArgs : EventArgs
        {
            public InetStream NewStream { private set; get; }
            public InetStream CurrentStream { private set; get; }
            public StreamChangingEventArgs(InetStream currentStream, InetStream newStream)
            {
                this.NewStream = newStream;
                this.CurrentStream = currentStream;
            }
        }

        #endregion


    }
}
